﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition.AttributedModel;
using System.ComponentModel.Composition.Caching;
using System.ComponentModel.Composition.Caching.AttributedModel;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using Microsoft.Internal;
using System.Globalization;

namespace System.ComponentModel.Composition
{
    /// <summary>
    ///     An immutable ComposablePartCatalog created from a type array or a list of managed types.  This class is threadsafe.
    ///     It is Disposable.
    /// </summary>
    public class AttributedTypesPartCatalog : ComposablePartCatalog, ICachedComposablePartCatalog
    {
        private readonly object _thisLock = new object();
        private Type[] _types = null;
        private volatile List<ComposablePartDefinition> _storage = null;
        private int _disposed = 0;
        private ComposablePartCatalog _cacheCatalog = null;

        /// <summary>
        ///     Initializes a new instance of the <see cref="AttributedTypesPartCatalog"/> class 
        ///     with the specified types.
        /// </summary>
        /// <param name="types">
        ///     An <see cref="Array"/> of attributed <see cref="Type"/> objects to add to the 
        ///     <see cref="AttributedTypesPartCatalog"/>.
        /// </param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="types"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///     <paramref name="types"/> contains an element that is <see langword="null"/>.
        ///     <para>
        ///         -or-
        ///     </para>
        ///     <paramref name="types"/> contains an element that was loaded in the Reflection-only context.
        /// </exception>
        public AttributedTypesPartCatalog(params Type[] types)
            : this((IEnumerable<Type>)types)
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="AttributedTypesPartCatalog"/> class
        ///     with the specified types.
        /// </summary>
        /// <param name="types">
        ///     An <see cref="IEnumerable{T}"/> of attributed <see cref="Type"/> objects to add 
        ///     to the <see cref="AttributedTypesPartCatalog"/>.
        /// </param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="types"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        ///     <paramref name="types"/> contains an element that is <see langword="null"/>.
        ///     <para>
        ///         -or-
        ///     </para>
        ///     <paramref name="types"/> contains an element that was loaded in the reflection-only context.
        /// </exception>
        public AttributedTypesPartCatalog(IEnumerable<Type> types)
        {
            Requires.NotNull(types, "types");

            foreach (Type type in types)
            {
                if (type == null)
                {
                    throw ExceptionBuilder.CreateContainsNullElement("types");
                }
#if !SILVERLIGHT
                if (type.Assembly.ReflectionOnly)
                {
                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.Argument_ElementReflectionOnlyType, "types"), "types");
                }
#endif
            }

            this._types = types.ToArray();
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="AttributedTypesPartCatalog"/> class 
        ///     with the specified cache.
        /// </summary>
        /// <param name="cache">
        ///     <see cref="ComposablePartCatalogCache"/> to read the <see cref="AttributedTypesPartCatalog"/> from.
        /// </param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="cache"/> is <see langword="null"/>.
        /// </exception>
        public AttributedTypesPartCatalog(ComposablePartCatalogCache cache)
        {
            Requires.NotNull(cache, "cache");
            ComposablePartCatalog cacheCatalog = cache.GetCacheCatalog(new AttributedComposablePartCatalogSite());

            // Process the cache - if any of the assemblies of the contained types has been changed, then
            // we can't use the cacheCatalog. Instead, we replace the definitions that are not up to date
            // with fully loaded ones. 
            // We loose the contract indexing, but at least we keep the definitions that are still up-to-date
            // in a cached state
            // NOTE : this is pretty heavy, as we will end up creating every single part definition, 
            // just to check that they are up-to-date
            List<ComposablePartDefinition> upToDatePartDefinitions = new List<ComposablePartDefinition>();
            bool areAllDefinitionsUpTodate = true;
            foreach (ComposablePartDefinition partDefinition in cacheCatalog.Parts)
            {
                ComposablePartDefinition upToDatePartDefinition = AttributedComposablePartCatalogSite.GetUpToDatePartDefinition(partDefinition);
                areAllDefinitionsUpTodate &= (partDefinition == upToDatePartDefinition);
                upToDatePartDefinitions.Add(upToDatePartDefinition);
            }

            if (areAllDefinitionsUpTodate)
            {
                this._cacheCatalog = cacheCatalog;
                this._storage = null;
            }
            else
            {
                this._cacheCatalog = null;
                this._storage = upToDatePartDefinitions;
            }
        }

        /// <summary>
        ///    Caches the catalog using the specified cache writer
        /// </summary>
        /// <param name="writer">
        ///    The <see cref="ComposablePartCatalogCacheWriter"/> to use to cache the 
        ///    <see cref="AttributedTypesPartCatalog"/>.
        /// </param>
        /// <returns></returns>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AttributedTypesPartCatalog"/> has been disposed of.
        /// </exception>
        public object CacheCatalog(ComposablePartCatalogCacheWriter writer)
        {
            ThrowIfDisposed();

            Requires.NotNull(writer, "writer");

            object cacheToken = writer.WriteCache(
                this.GetType(),
                this.Parts,
                null,
                new AttributedComposablePartCatalogSite());

            writer.WriteRootCacheToken(cacheToken);
            return cacheToken;
        }

        /// <summary>
        ///     Gets a value indicating whether the underlying cache is up-to-date.
        /// </summary>
        /// <value>
        ///     <see langword="true"/> if the underlying <see cref="ComposablePartCatalogCache"/> 
        ///     is up-to-date; otherwise, <see langword="false"/>.
        /// </value>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AttributedTypesPartCatalog"/> has been disposed of.
        /// </exception>
        public bool IsCacheUpToDate
        {
            get 
            {
                ThrowIfDisposed();

                return (this._cacheCatalog != null); 
            }
        }

        /// <summary>
        ///     Gets the part definitions of the catalog.
        /// </summary>
        /// <value>
        ///     A <see cref="IQueryable{T}"/> of <see cref="ComposablePartDefinition"/> objects of the 
        ///     <see cref="AttributedTypesPartCatalog"/>.
        /// </value>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AttributedTypesPartCatalog"/> has been disposed of.
        /// </exception>
        public override IQueryable<ComposablePartDefinition> Parts
        {
            get
            {
                ThrowIfDisposed();

                // If the cache is present, we read from that
                if (this._cacheCatalog != null)
                {
                    return this._cacheCatalog.Parts;
                }

                if (this._storage == null)
                {
                    lock (this._thisLock)
                    {
                        if (this._storage == null)
                        {
                            var collection = new List<ComposablePartDefinition>();
                            foreach (Type type in this._types)
                            {
                                var definition = new AttributedComposablePartDefinition(type);
                                if (definition.IsComponentDiscoverable())
                                {
                                    collection.Add(definition);
                                }
                            }
                            this._types = null;
                            this._storage = collection;
                        }
                    }
                }

                return this._storage.AsQueryable();
            }
        }

        /// <summary>
        ///     Returns the export definitions that match the conditions defined by the specified constraint.
        /// </summary>
        /// <param name="constraint">
        ///     A <see cref="Expression{TDelegate}"/> containing a <see cref="Func{T, TResult}"/> 
        ///     that defines the conditions of the <see cref="ExportDefinition"/> objects to return.
        /// </param>
        /// <returns>
        ///     An <see cref="IEnumerable{T}"/> of <see cref="Tuple{TFirst, TSecond}"/> containing the 
        ///     <see cref="ExportDefinition"/> objects and their associated 
        ///     <see cref="ComposablePartDefinition"/> objects that matche the conditions defined 
        ///     by <paramref name="constraint"/>.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="constraint"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AttributedTypesPartCatalog"/> has been disposed of.
        /// </exception>
        public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(Expression<Func<ExportDefinition, bool>> constraint)
        {
            ThrowIfDisposed();

            Requires.NotNull(constraint, "constraint");

            // If the cache is present, we read from that
            if (this._cacheCatalog != null)
            {
                return this._cacheCatalog.GetExports(constraint);
            }
            else
            {
                return base.GetExports(constraint);
            }
        }

        protected override void Dispose(bool disposing)
        {
            try
            {
                if (Interlocked.CompareExchange(ref this._disposed, 1, 0) == 0)
                {
                    if (disposing)
                    {
                        if (this._cacheCatalog != null)
                        {
                            this._cacheCatalog.Dispose();
                        }
                    }
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        private void ThrowIfDisposed()
        {
            if (this._disposed == 1)
            {
                throw ExceptionBuilder.CreateObjectDisposed(this);
            }
        }
    }
}
